iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0
Modern Web

使用 Kotlin 快速開發 Web 程式 -- Vaadin系列 第 23

立委名單/提案 Open Data / CsvToBean - day23

  • 分享至 

  • xImage
  •  

目標

今天寫個時事題,我們來查詢立委議案提案
https://ithelp.ithome.com.tw/upload/images/20211008/20138680SjT2vdaWvw.png

本日重點

  • 立法院 Open Data
  • 如何將 CSV 轉成 Bean
  • DataLoader 多欄位過濾
  • ListBox
  • 自訂 ListBox

為節省篇幅,本示例僅列出關鍵程式碼,畫面編排、按鍵 onclick、DataLoader、Grid顯示...等使用方法,請參考本系列其他文章。

資料來源 : 立法院開放資料服務平台

查詢現任立委

立法院提供三種資料模式,分別是 csv、txt、xls (雖然立法院開放資料服務平台上有JSON選項,但並未提供內容)
https://ithelp.ithome.com.tw/upload/images/20211008/20138680up3sbyTv4l.png

從網站資訊得知此api提供欄位如下 :

| name|委員姓名||ename|委員英文姓名|
|-|-|-|-|
| sex|性別|| party|黨籍|
| partyGroup|黨團|| areaName|選區名稱|
| committee|委員會|| onboardDate|到職日(西元年)|
| degree|學歷|| tel|電話|
| experience|經歷|| fax|傳真|
| addr|通訊處|| picUrl|照片位址|
| leaveFlag|是否離職|| leaveDate|離職日期(西元年)|
| leaveReason|離職原因|

建立資料表

  • 建立存放立委資訊資料表Legislator,請開新檔 V05__CreateLegislator.sql
    create TABLE Legislator(
      id bigint auto_increment PRIMARY KEY,
      term VARCHAR(3),
      name VARCHAR(50) NOT NULL,
      sex VARCHAR(10) NOT NULL,
      party VARCHAR(100),
      partyGroup VARCHAR(100),
      areaName VARCHAR(100),
      committee VARCHAR(500),
      degree VARCHAR(200),
      tel VARCHAR(200),
      experience VARCHAR(1000),
      addr VARCHAR(500),
      picUrl VARCHAR(100),
      leaveFlag VARCHAR(10),
      leaveDate VARCHAR(20),
      leaveReason VARCHAR(1000)
    );
  • 建立對應的KEntity
data class Legislator(
    override var id: Long? = null,
    var term: String? = null,
    var name: String? = null,
    var sex: String? = null,
    var party: String? = null,
    var partyGroup: String? = null,
    var areaName: String? = null,
    var committee: String? = null,
    var degree: String? = null,
    var tel: String? = null,
    var experience: String? = null,
    var addr: String? = null,
    var picUrl: String? = null,
    var leaveFlag: String? = null,
    var leaveDate: String? = null,
    var leaveReason: String? = null
):KEntity<Long> ,Serializable{
    companion object: Dao<Legislator, Long>(Legislator::class.java)
}

下載資料並轉檔

CSV 轉成 Bean 的資料轉換使用 OpenCSV 套件,請開啟 build.gradle.kts,導入opencsv 套件

    implementation("com.opencsv:opencsv:5.5.2")

將下載寫在download()方法,轉檔後儲存到資料表Legislator

    fun download(){
        URL("https://data.ly.gov.tw/odw/usageFile.action?id=9&type=CSV&fname=9_CSV.csv")
            .readText().apply {
                CsvToBeanBuilder<Legislator>(StringReader(this))
                    .withType(Legislator::class.java)
                    .withSeparator(',')
                    .withIgnoreLeadingWhiteSpace(true)
                    .withIgnoreEmptyLine(true)
                    .build()
                    .parse().forEach {
                        it.save()
                    }
            }
    }

第2,3行,從立法院開放資料服務平台取得資料
第4行,使用CsvToBeanBuilder()方法進行轉換,參數型態為Reader,所以將下載回來的字串進行轉換
第5行,parser型態
第6行,分隔符號
第7行,忽略前置空白
第8行,忽略空白行
第11行,因LegislatorKEntity類,直接使用save()方法儲存到資料庫。

將下載資訊以 Grid 顯示

設定 Grid 資料來源為 EntityDataLoader,首次執行時因資料表 Legislator 尚無任何資料,grid為空白。

    grid = grid<Legislator> {
        setDataLoader(Legislator.dataLoader)
        addColumnFor(Legislator::name).setHeader("委員姓名")
       (略)    
    }

下載立委資料並儲存後,更新Grid

    download()
    grid.dataProvider.refreshAll()

展開立委 Detail

點選grid row 展開detail,希望detail顯示相片、所屬委員會、服務處地址等資訊。

所屬委員會

所屬委員會欄位committee內容如下:

第10屆第1會期:經濟委員會;第10屆第2會期:經濟委員會;第10屆第3會期:經濟委員會;第10屆第3會期:紀律委員會;第10屆第4會期:經濟委員會;

;分隔每筆資料,先將資料整理一下再放入List,最後以ListBox顯示

    val committees = mutableListOf<String>()
    data.committee?.let {
        it.split(";").forEach { committees.add(it) }
    }
    val listBox = ListBox<String>()
    listBox.setWidth("35%")
    listBox.setItems(committees)

完整內容如下

    setSelectionMode(Grid.SelectionMode.NONE)
    val renderer = ComponentRenderer<HorizontalLayout, Legislator> { data: Legislator ->
        val horizontalLayout = HorizontalLayout()
        val image = Image()
        with(image){
            isExpand = false
            width = "10%"
            setMaxWidth(200F, Unit.PIXELS)
            setMaxHeight(200F, Unit.PIXELS)
            if (!data.picUrl.isNullOrEmpty())
                src = data.picUrl
        }
        horizontalLayout.add(image)

        val committees = mutableListOf<String>()
        data.committee?.let {
            it.split(";").forEach { committees.add(it) }
        }
        val listBox = ListBox<String>()
        listBox.setWidth("35%")
        listBox.setItems(committees)
        horizontalLayout.add(listBox)

        val listBox_addr = ListBox<String>()
        val addrs = mutableListOf<String>()
        data.addr?.let {
            it.split(";").forEach { addrs.add(it) }
        }
        listBox_addr.setWidth("50%")
        listBox_addr.setItems(addrs)
        horizontalLayout.add(listBox_addr)

        horizontalLayout
    }
    setItemDetailsRenderer(renderer)

執行結果

https://ithelp.ithome.com.tw/upload/images/20211008/20138680wWKMiRaAp4.png

Refactor - 自訂 Component

上述程式中,ListBox 不同的資料來源,皆須將資料切分後,使用ListBox顯示,程式碼看起來不僅不親民、兀長,且未能 reuse。複習一下先前曾學過的 Custom Component,Refactor 一下,將 ListBox 改為 Component。

開新檔ListComponent.kt

class ListComponent(data: String, split: String, title: String) : KComposite() {
    val list = mutableListOf<String>()
    private val root = ui {
        data.split(split).forEach {
            if (!it.isNullOrEmpty()) list.add(it.trim())
        }
        verticalLayout {
            label(title)
            listBox<String> { setItems(list) }
        }
    }
}

fun HasComponents.listComponent(
    data: String,
    split: String,
    title: String,
    block: ListComponent.() -> kotlin.Unit = {}
) = init(ListComponent(data, split, title), block)

傳入三個參數,data、split、title

使用

上述兀長的資料轉換、加入ListBox()等程式內容,只要改成這樣就行了,是不是簡單易讀多了。

    horizontalLayout.add(
        ListComponent(data.committee!!, ";", "委員會" )
    )

    horizontalLayout.add(
        ListComponent(data.addr!!, ";", "服務處")
    )

查詢議案提案

提供欄位有

term 屆別 sessionPeriod 會期 sessionTimes 會次
meetingTimes 臨時會會次 billNo 議案編號 billName 提案名稱
billOrg 提案單位/委員 billProposer 提案人(委員或黨團) billCosignatory 連署人
billStatus 議案狀態 pdfUrl 關係文書pdf檔案下載位置 docUrl 關係文書doc檔案下載位置
selectTerm 屆別期別篩選條件

建立資料表 ProposalOfBills

create TABLE ProposalOfBills(
    id bigint auto_increment PRIMARY KEY,
    billNo VARCHAR(20),
    billName VARCHAR(500),
    billOrg VARCHAR(50),
    billProposer VARCHAR(200),
    billCosignatory VARCHAR(1000),
    billStatus VARCHAR(10),
    pdfUrl VARCHAR(100),
    docUrl VARCHAR(100)
);

新增 KEntityProposalOfBills

data class ProposalOfBills(
    override var id: Long? = null,
    var billNo: String? = null,
    @field:NotNull
    var billName: String? = null,
    @field:NotNull
    var billOrg: String? = null,
    var billProposer: String? = null,
    var billCosignatory: String? = null,
    var billStatus: String? = null,
    var pdfUrl: String? = null,
    var docUrl: String? = null
): KEntity<Long>, Serializable{
    companion object: Dao<ProposalOfBills, Long>(ProposalOfBills::class.java)
}

下載

下載的部份和下載委員資料大同小異,僅列出和上述不同的部份

    .withThrowExceptions(false)

從前述資料可知,提供的資料欄位數遠大於所需,故只儲存部份欄位備用。但是這樣一來,parse 過程會因為欄位對應不上而拋出 Exceptions,為免轉檔因此中斷。所以加上這一行讓parse可繼續進行。

過濾委員姓名

輸入委員姓名後要過濾兩個欄位,提案人(billProposer)和連署人(billCosignatory)

filter = textField {
    placeholder = "委員姓名"
    addValueChangeListener { event ->
        var dp: DataLoader<ProposalOfBills> = ProposalOfBills.dataLoader
        if (!filter.value.isBlank())
            dp = dp.withFilter {
                (ProposalOfBills::billProposer contains filter.value) or
                        (ProposalOfBills::billCosignatory contains  filter.value)
            }
        grid.setDataLoader(dp)
    }
    valueChangeMode = ValueChangeMode.EAGER
    isExpand = true
}
setVerticalComponentAlignment(FlexComponent.Alignment.START, filter)

第4行,取得EntityDataLoader
第5行,若欄位不為空,加過濾條件
第6行,加條件
第7,8行,我們要找的資料是委員姓名在欄位裡任一位置都可以,所以使用 contains 比對

展開提案 Detail

    val renderer = ComponentRenderer<HorizontalLayout, ProposalOfBills> { data: ProposalOfBills ->
        val horizontalLayout = HorizontalLayout()
        horizontalLayout.add(
            ListComponent(data.billProposer!!, "  ", "提案人(委員或黨團)")
        )
        horizontalLayout.add(
            ListComponent(data.billCosignatory!!, "  ", "連署人")
        )
        horizontalLayout
    }

方才寫好的 ListComponent 又派上用場了

執行結果

https://ithelp.ithome.com.tw/upload/images/20211008/20138680UXd2O0OUwa.png

https://ithelp.ithome.com.tw/upload/images/20211008/20138680LzD8Rgj4La.png


上一篇
如何讓 Grid 顯示關聯式 SQL 查詢的資料 - day22
下一篇
廣播推送 - day24
系列文
使用 Kotlin 快速開發 Web 程式 -- Vaadin30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言